/* ===========================================================
 * TradeManager : a application to trade strategies for the Java(tm) platform
 * ===========================================================
 *
 * (C) Copyright 2011-2011, by Simon Allen and Contributors.
 *
 * Project Info:  org.trade
 *
 * This library is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
 * License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
 * USA.
 *
 * [Java is a trademark or registered trademark of Oracle, Inc.
 * in the United States and other countries.]
 *
 * (C) Copyright 2011-2011, by Simon Allen and Contributors.
 *
 * Original Author:  Simon Allen;
 * Contributor(s):   -;
 *
 * Changes
 * -------
 *
 */
package org.trade.core.valuetype;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Comparator;

import org.trade.core.conversion.JavaTypeTranslator;
import org.trade.core.message.IMessageFactory;
import org.trade.core.util.CoreUtils;
import org.trade.core.validator.ExceptionMessageListener;
import org.trade.core.validator.PercentValidator;
import org.trade.core.validator.Validator;

/**
 */
public class Percent extends ValueType implements Comparator<Percent>,
		Comparable<Percent> {
	/**
	 * 
	 */
	private static final long serialVersionUID = 6356086072126179279L;

	public final static String PERCENT_POSITIVE_7_2 = "($)#(,)###(,)###(.##)";

	public final static String PERCENT_NONNEGATIVE_8_2 = "($)##(,)###(,)###(.##)";

	public final static String PERCENT_POSITIVE_10_2 = "($)#(,)###(,)###(,)###(.##)";

	public final static String PERCENT_NONNEGATIVE_11_2 = "($)##(,)###(,)###(,)###(.##)";

	public final static Percent ZERO = new Percent(0L, 0);

	protected static Boolean m_ascending = new Boolean(true);

	static {
		// Register the appropriate converters
		JavaTypeTranslator.registerDynamicTypeConverter(new ObjectToPercent());
		JavaTypeTranslator.registerDynamicTypeConverter(new PercentToObject());
	}

	//
	// Private Attributes
	//

	private BigDecimal m_value = null;

	private String m_format = PERCENT_NONNEGATIVE_11_2;

	private String m_invalidValue = null; // This will be null if there were

	// no conversion errors

	private final static int SCALE = 6;

	private final static String MULTIPLIER = "100";

	//
	// Public Methods
	//

	/**
	 * Default Constructor. Create an object and initialize it to empty.
	 */
	public Percent() {
	}

	/**
	 * Default Constructor. Create an object and initialize it to empty.
	 * 
	 * @param PercentString
	 *            String
	 */
	public Percent(String PercentString) {
		if ((null != PercentString) && (PercentString.length() != 0)) {
			// This is necessary because Java will parse strings with multiple
			// dashes
			if (PercentString.indexOf("-") != PercentString.lastIndexOf("-")) {
				m_invalidValue = PercentString;
			} else {
				try {
					setBigDecimal(new BigDecimal(PercentString));
				} catch (NumberFormatException e) {
					m_invalidValue = PercentString;
				}
			}
		}
	}

	/**
	 * Constructor
	 * 
	 * 
	 * @param d
	 *            Integer
	 */
	public Percent(Integer d) {
		setBigDecimal(d);
	}

	/**
	 * Constructor
	 * 
	 * 
	 * @param d
	 *            double
	 */
	public Percent(double d) {
		setBigDecimal(new BigDecimal(d));
	}

	/**
	 * Constructor
	 * 
	 * 
	 * @param d
	 *            Double
	 */
	public Percent(Double d) {
		setBigDecimal(new BigDecimal(d.doubleValue()));
	}

	/**
	 * Constructor
	 * 
	 * 
	 * @param bd
	 *            BigDecimal
	 */
	public Percent(BigDecimal bd) {
		setBigDecimal(bd);
	}

	/**
	 * Constructor for Percent.
	 * 
	 * @param Percent
	 *            Percent
	 */
	public Percent(Percent Percent) {
		m_value = Percent.m_value;
		m_format = Percent.m_format;
		m_invalidValue = Percent.m_invalidValue;
	}

	/**
	 * Constructor
	 * 
	 * 
	 * 
	 * @param nonDecimalAmount
	 *            long
	 * @param decimalAmount
	 *            int
	 */
	public Percent(long nonDecimalAmount, int decimalAmount) {
		// Set up the default constraints for basic Percent values
		BigDecimal val = new BigDecimal((nonDecimalAmount * 100)
				+ decimalAmount);
		setBigDecimal(val.movePointLeft(SCALE));
	}

	/**
	 * Provides the format used for determining if this object is valid. The
	 * format should be one of the format constants on this class. The default
	 * format is NORMAL_11_2.
	 * 
	 * @param format
	 *            String
	 */
	public void setFormat(String format) {
		m_format = format;
	}

	/**
	 * Method getFormat.
	 * 
	 * @return String
	 */
	public String getFormat() {
		return m_format;
	}

	/**
	 * This maximum length includes the decimal point and digits to both sides.
	 * 
	 * @return int
	 */
	public int getMaxLength() {
		int maxLength = 14;

		if (getFormat().equals(PERCENT_NONNEGATIVE_8_2)) {
			maxLength = 11;
		} else if (getFormat().equals(PERCENT_POSITIVE_10_2)) {
			maxLength = 13;
		} else if (getFormat().equals(PERCENT_POSITIVE_7_2)) {
			maxLength = 10;
		}

		return maxLength;
	}

	/**
	 * This indicates whether zero is an acceptable value for this instance.
	 * Currently this is determined by the format returned by getFormat().
	 * 
	 * @return boolean
	 */
	public boolean canBeZero() {
		boolean zero = true;

		if (getFormat().equals(PERCENT_POSITIVE_7_2)) {
			zero = false;
		} else if (getFormat().equals(PERCENT_POSITIVE_10_2)) {
			zero = false;
		}

		return zero;
	}

	/**
	 * This indicates whether zero is an acceptable value for this instance.
	 * Currently this is determined by the format returned by getFormat().
	 * 
	 * @return boolean
	 */
	public boolean canBeNegative() {
		boolean negative = false;

		// Currently all formats prohibit negative numbers.

		return negative;
	}

	/**
	 * Method isNegative.
	 * 
	 * @return boolean
	 */
	public boolean isNegative() {
		assertDefined();

		boolean negative = false;

		if (m_value.compareTo(new BigDecimal(0)) < 0) {
			negative = true;
		}

		return negative;
	}

	/**
	 * Method isEmpty.
	 * 
	 * @return boolean
	 */
	public boolean isEmpty() {
		boolean empty = false;

		if ((null == m_value) || (null != m_invalidValue)) {
			empty = true;
		}

		return empty;
	}

	/**
	 * 
	 * @return The value before the decimal point in the Percent value.
	 */
	public long getNonDecimalAmount() {
		assertDefined();

		long nonDecimalAmount = 0;

		if (null != m_value) {
			nonDecimalAmount = m_value.longValue();
		}

		return nonDecimalAmount;
	}

	/**
	 * See description of superclass method. Overrode functionality to return
	 * the BigDecimal this object is using intrnally.
	 * 
	 * @return Object
	 */
	public Object getSQLObject() {
		return (getBigDecimalValue());
	}

	/**
	 * 
	 * @return The value after the decimal point in the Percent value.
	 */
	public int getDecimalAmount() {
		assertDefined();

		int decimalAmount = 0;

		if (null != m_value) {
			BigInteger tot = (m_value.movePointRight(SCALE)).toBigInteger();
			BigInteger sub = m_value.toBigInteger();
			sub = sub.multiply(new BigInteger(MULTIPLIER));

			BigInteger res = tot.subtract(sub);

			decimalAmount = res.intValue();
		}

		return decimalAmount;
	}

	/**
	 * Will throw a <code>NullPointerException</code> if this valuetype is
	 * empty.
	 * 
	 * 
	 * @return A BigDecimal representing the monetary value.
	 */
	public BigDecimal getBigDecimalValue() {
		assertDefined();

		return m_value;
	}

	/**
	 * Method toString.
	 * 
	 * @return String
	 */
	public String toString() {
		if (null != m_value) {
			return (m_value.toString());
		} else if (null != m_invalidValue) {
			return m_invalidValue;
		} else {
			return "";
		}
	}

	/**
	 * Method setValue.
	 * 
	 * @param value
	 *            Object
	 * @throws ValueTypeException
	 */
	public void setValue(Object value) throws ValueTypeException {
		if (value instanceof Percent) {
			setBigDecimal(((Percent) value).m_value);
		} else {
			try {
				setBigDecimal(((Percent) JavaTypeTranslator.convert(
						Percent.class, value)).getBigDecimalValue());
			} catch (Exception ex) {
				throw new ValueTypeException(ex);
			}
		}
	}

	/**
	 * Adds two Percent objects
	 * 
	 * @param Percent
	 *            the Percent object to be added
	 * 
	 * 
	 * @return Percent the result
	 */
	public Percent add(Percent Percent) {
		assertDefined();

		if (null == m_value) {
			if (null == Percent.getBigDecimalValue()) {
				return new Percent();
			} else {
				return new Percent(Percent.getBigDecimalValue());
			}
		}

		BigDecimal value = m_value.add(Percent.getBigDecimalValue());
		return new Percent(value);
	}

	/**
	 * Subtracts two Percent objects
	 * 
	 * @param Percent
	 *            the Percent object to be subtracted
	 * 
	 * 
	 * @return Percent the result
	 */
	public Percent subtract(Percent Percent) {
		assertDefined();

		if (null == m_value) {
			return (Percent);
		}

		BigDecimal value = m_value.subtract(Percent.getBigDecimalValue());
		return new Percent(value);
	}

	/**
	 * Compares two Percent objects.
	 * 
	 * @param Percent
	 *            the Percent object to compare with.
	 * 
	 * 
	 * @return boolean result.
	 */
	public boolean isLessThen(Percent Percent) {
		assertDefined();

		BigDecimal thisValue = notNull(this);
		BigDecimal parameter = notNull(Percent);

		return (thisValue.compareTo(parameter) < 0);
	}

	/**
	 * Compares two Percent objects.
	 * 
	 * @param Percent
	 *            the Percent object to compare with.
	 * 
	 * 
	 * @return boolean result.
	 */
	public boolean isLessThenOrEqualTo(Percent Percent) {
		assertDefined();

		BigDecimal thisValue = notNull(this);
		BigDecimal parameter = notNull(Percent);

		return (thisValue.compareTo(parameter) <= 0);
	}

	/**
	 * Compares two Percent objects.
	 * 
	 * @param Percent
	 *            the Percent object to compare with.
	 * 
	 * 
	 * @return boolean result.
	 */
	public boolean isGreaterThen(Percent Percent) {
		assertDefined();

		BigDecimal thisValue = notNull(this);
		BigDecimal parameter = notNull(Percent);

		return (thisValue.compareTo(parameter) > 0);
	}

	/**
	 * Compares two Percent objects.
	 * 
	 * @param Percent
	 *            the Percent object to compare with.
	 * 
	 * 
	 * @return boolean result.
	 */
	public boolean isGreaterThenOrEqualTo(Percent Percent) {
		assertDefined();

		BigDecimal thisValue = notNull(this);
		BigDecimal parameter = notNull(Percent);

		return (thisValue.compareTo(parameter) >= 0);
	}

	/**
	 * Method isValid.
	 * 
	 * @param validator
	 *            Validator
	 * @param receiver
	 *            ExceptionMessageListener
	 * @return boolean
	 */
	public boolean isValid(Validator validator,
			ExceptionMessageListener receiver) {
		return validator.isValid(m_value, m_invalidValue, null, receiver);
	}

	/**
	 * Method getDefaultValidator.
	 * 
	 * @param messageFactory
	 *            IMessageFactory
	 * @param isMandatory
	 *            boolean
	 * @return Validator
	 */
	public Validator getDefaultValidator(IMessageFactory messageFactory,
			boolean isMandatory) {
		// This allow non-negative 11.2
		return new PercentValidator(messageFactory, false, true, 7, 5,
				isMandatory);
	}

	/**
	 * @deprecated Use the new validator method instead.
	 * @return boolean
	 */
	public boolean isValid() {
		boolean valid = false;

		String error = getError();
		if (null == error) {
			valid = true;
		}

		return valid;
	}

	/**
	 * @deprecated Use the new validator method instead.
	 * @return String
	 */
	public String getError() {
		String error = null;

		if (!isEmpty()) {
			long nonDecimalLength = Long.toString(getNonDecimalAmount())
					.length();

			// Note that the decimal length will be 1 for 00-09.
			long decimalLength = Long.toString(getDecimalAmount()).length();

			// Allow only 2 decimal places.
			if (decimalLength > 2) {
				error = "only two decimal places are allowed";
			}

			// Add three to account for the decimal portion and decimal point.
			if ((nonDecimalLength + 3) > getMaxLength()) {
				error = "length of digits and decimal point should not exceed "
						+ getMaxLength();
			}

			// Disallow zero for certain formats
			if (!canBeZero() && (getBigDecimalValue().doubleValue() == 0)) {
				error = "amount cannot be zero";
			}

			// Disallow negative numbers
			if (!canBeNegative() && (getBigDecimalValue().doubleValue() < 0)) {
				error = "amount cannot be negative";
			}
		}

		return error;
	}

	/**
	 * Will throw a <code>NullPointerException</code> if this valuetype is
	 * empty.
	 * 
	 * 
	 * @return A double representing the monetary value.
	 */

	public double doubleValue() {
		assertDefined();
		return m_value.doubleValue();
	}

	/**
	 * Overrides Cloneable
	 * 
	 * 
	 * 
	 * 
	 * 
	 * 
	 * @return Object
	 * @exception * @see
	 */

	public Object clone() {
		try {
			Percent other = (Percent) super.clone();
			return other;
		} catch (CloneNotSupportedException e) {
			// will never happen
			return null;
		}
	}

	/**
	 * Method compareTo.
	 * 
	 * @param other
	 *            Percent
	 * @return int
	 */
	public int compareTo(final Percent other) {
		return CoreUtils.nullSafeComparator(this.getBigDecimalValue(),
				other.getBigDecimalValue());
	}

	/**
	 * Method compare.
	 * 
	 * @param o1
	 *            Percent
	 * @param o2
	 *            Percent
	 * @return int
	 */
	public int compare(Percent o1, Percent o2) {
		int returnVal = CoreUtils.nullSafeComparator(o1.getBigDecimalValue(),
				o2.getBigDecimalValue());
		if (m_ascending.equals(Boolean.FALSE)) {
			returnVal = returnVal * -1;
		}
		return returnVal;
	}

	/**
	 * Method equals.
	 * 
	 * @param objectToCompare
	 *            Object
	 * @return boolean
	 */
	public boolean equals(Object objectToCompare) {

		if (super.equals(objectToCompare))
			return true;

		if (objectToCompare instanceof Percent) {
			if (CoreUtils.nullSafeComparator(
					((Percent) objectToCompare).getBigDecimalValue(),
					this.getBigDecimalValue()) == 0)
				return true;
		}
		return false;
	}

	//
	// Private Methods
	//
	/**
	 * Method setBigDecimal.
	 * 
	 * @param value
	 *            Integer
	 */
	private void setBigDecimal(Integer value) {
		if (null == value) {
			m_value = new BigDecimal(0.0);
		} else {
			// m_value = value;
			m_value = (new BigDecimal(value)).setScale(SCALE,
					BigDecimal.ROUND_HALF_EVEN);
		}

		// Clear any invalid values
		m_invalidValue = null;
	}

	/**
	 * Method setBigDecimal.
	 * 
	 * @param value
	 *            BigDecimal
	 */
	private void setBigDecimal(BigDecimal value) {
		if (null == value) {
			m_value = new BigDecimal(0.0);
		} else {
			// m_value = value;
			m_value = value.setScale(SCALE, BigDecimal.ROUND_HALF_EVEN);
		}

		// Clear any invalid values
		m_invalidValue = null;
	}

	/**
	 * Method notNull.
	 * 
	 * @param value
	 *            Percent
	 * @return BigDecimal
	 */
	private BigDecimal notNull(Percent value) {
		if (null == value) {
			return (new BigDecimal(0.0D));
		} else {
			return (value.getBigDecimalValue());
		}
	}

	private void assertDefined() {
		if (null != m_invalidValue) {
			throw new NumberFormatException(
					"Attempting to use a Percent that was not properly initialized.  Invalid value is: "
							+ m_invalidValue);
		}
	}
}
